/*
 * Copyright (C) 2010-2011 Comcast Cable.  All Rights Reserved.
 */
package com.hercules.android.receiver.handlers.commands.call.encryption;

import android.content.Context;

import com.hercules.android.receiver.AndroidReceiver;
import com.hercules.android.receiver.handlers.commands.call.Helper;
import com.hercules.android.receiver.handlers.commands.call.ObjectEncryptor;
import com.hercules.android.receiver.handlers.commands.call.ObjectEncryptorException;

import java.io.ByteArrayInputStream;
import java.io.ByteArrayOutputStream;
import java.io.FileNotFoundException;
import java.io.IOException;
import java.io.ObjectInputStream;
import java.io.ObjectOutputStream;
import java.io.Serializable;
import java.security.GeneralSecurityException;
import java.security.NoSuchAlgorithmException;
import java.util.Arrays;

import javax.crypto.Cipher;
import javax.crypto.KeyGenerator;
import javax.crypto.SealedObject;
import javax.crypto.SecretKey;

/**
 * This class is a concrete implementation of ObjectEncryptor interface. It uses javax.crypto to perform
 * encryption/decryption of objects.
 * <p>
 * <b>Thread Safety:</b> Not thread safe since it is mutable. Setters are meant to be used for initialization purposes
 * only and in this conditions the class can be used in a thread safe manner.
 *
 * @author DanLazar, orial
 * @version 1.0
 */
public class DefaultObjectEncryptor implements ObjectEncryptor {
    /**
     * The encryption algorithm name.
     * <p>
     * Initially it is set to "AES". Has a setter and a getter. Cannot be null or empty.
     */
    private String encryptionAlgorithmName = "AES";

    /**
     * The encryption key file.
     * <p>
     * Has a setter and a getter. Cannot be null or empty. Initially it is set to "key".
     */
    private String encryptionKeyFile = "key";

    /**
     * Default empty constructor.
     */
    public DefaultObjectEncryptor() {
        // empty
    }

    /**
     * Gets the encryption algorithm name.
     *
     * @return the encryption algorithm name
     */
    public String getEncryptionAlgorithmName() {
        return encryptionAlgorithmName;
    }

    /**
     * Sets the encryption algorithm name.
     *
     * @param encryptionAlgorithmName the encryption algorithm name
     * @throws IllegalArgumentException if parameter is null or empty string
     */
    public void setEncryptionAlgorithmName(String encryptionAlgorithmName) {
        // check argument
        Helper.checkString(encryptionAlgorithmName, "encryptionAlgorithmName");

        this.encryptionAlgorithmName = encryptionAlgorithmName;
    }

    /**
     * Gets the encryption key file.
     *
     * @return the encryption key file
     */
    public String getEncryptionKeyFile() {
        return encryptionKeyFile;
    }

    /**
     * Sets the encryption key file.
     *
     * @param encryptionKeyFile the encryption key file
     * @throws IllegalArgumentException if parameter is null or empty string
     */
    public void setEncryptionKeyFile(String encryptionKeyFile) {
        // check argument
        Helper.checkString(encryptionKeyFile, "encryptionKeyFile");

        this.encryptionKeyFile = encryptionKeyFile;
    }

    /**
     * Encrypts the given object.
     *
     * @param object the object to be encrypted
     * @param receiver the current android receiver
     * @return the encrypted object as an array of bytes
     * @throws IllegalArgumentException if any parameter is null or receiver is incorrect
     * @throws ObjectEncryptorException if any errors occur while encrypting the object
     */
    public byte[] encrypt(Serializable object, AndroidReceiver receiver) throws ObjectEncryptorException {
        // check arguments
        Helper.checkNotNull(object, "object");
        Helper.checkNotNull(receiver, "receiver");
        Helper.checkNotNull(receiver.getRootView(), "receiver.rootView");

        ObjectOutputStream output = null;
        try {
            // get encryption cipher
            Cipher ecipher = getCipher(Cipher.ENCRYPT_MODE, receiver.getRootView().getContext());

            // encrypt the object
            SealedObject sealedObject = new SealedObject(object, ecipher);

            // return encrypted object as byte array
            ByteArrayOutputStream bytes = new ByteArrayOutputStream();
            output = new ObjectOutputStream(bytes);
            output.writeObject(sealedObject);
            return bytes.toByteArray();
        } catch (IOException e) {
            throw new ObjectEncryptorException("I/O error occurred while encrypting object '" + object
                + "', algorithm '" + encryptionAlgorithmName + "', key file '" + encryptionKeyFile + "'.", e);
        } catch (GeneralSecurityException e) {
            throw new ObjectEncryptorException("Security error occurred while encrypting object '" + object
                + "', algorithm '" + encryptionAlgorithmName + "', key file '" + encryptionKeyFile + "'.", e);
        } finally {
            Helper.closeStream(output);
        }
    }

    /**
     * Decrypts the given object.
     *
     * @param object the object, as an array of bytes, to be decrypted
     * @param receiver the current android receiver
     * @return the decrypted object
     * @throws IllegalArgumentException if object is null or empty or receiver is incorrect
     * @throws ObjectEncryptorException if any errors occur while decrypting the object
     */
    public Serializable decrypt(byte[] object, AndroidReceiver receiver) throws ObjectEncryptorException {
        // check arguments
        Helper.checkByteArray(object, "object");
        Helper.checkNotNull(receiver, "receiver");
        Helper.checkNotNull(receiver.getRootView(), "receiver.rootView");

        ObjectInputStream input = null;
        try {
            // get decryption cipher
            Cipher dcipher = getCipher(Cipher.DECRYPT_MODE, receiver.getRootView().getContext());

            // create SealedObject instance from object byte array
            input = new ObjectInputStream(new ByteArrayInputStream(object));
            SealedObject sealedObject = (SealedObject) input.readObject();

            // decrypt the object
            return (Serializable) sealedObject.getObject(dcipher);
        } catch (ClassNotFoundException e) {
            throw new ObjectEncryptorException("Class, sealed in byte array is not found.", e);
        } catch (IOException e) {
            throw new ObjectEncryptorException("I/O error occurred while decrypting bytes '" + object
                + "', algorithm '" + encryptionAlgorithmName + "', key file '" + encryptionKeyFile + "'.", e);
        } catch (GeneralSecurityException e) {
            throw new ObjectEncryptorException("Security error occurred while decrypting bytes '"
                + Arrays.toString(object) + "', algorithm '" + encryptionAlgorithmName + "', key file '"
                + encryptionKeyFile + "'.", e);
        } finally {
            Helper.closeStream(input);
        }
    }

    /**
     * Gets secret key for sealed objects.
     *
     * @param generate true indicates, that key file could be generated
     * @param context the android context
     * @return the secret key
     * @throws IOException if I/O error occurs
     * @throws NoSuchAlgorithmException if encryption algorithm cannot be found
     * @throws ObjectEncryptorException if any other error occurs
     */
    private SecretKey getKey(boolean generate, Context context) throws IOException, ObjectEncryptorException,
        NoSuchAlgorithmException {
        ObjectInputStream input = null;
        ObjectOutputStream output = null;

        try {
            try {
                // read key from key file
                input = new ObjectInputStream(context.openFileInput(encryptionKeyFile));
                return (SecretKey) input.readObject();
            } catch (FileNotFoundException e) {
                // check if key file can be generated
                if (!generate) {
                    throw new ObjectEncryptorException("Key file '" + encryptionKeyFile + "' does not exist.");
                }

                // create encryption key
                SecretKey key = KeyGenerator.getInstance(encryptionAlgorithmName).generateKey();

                // write key to file
                output = new ObjectOutputStream(context.openFileOutput(encryptionKeyFile, Context.MODE_PRIVATE));
                output.writeObject(key);
                return key;
            }
        } catch (SecurityException e) {
            throw new ObjectEncryptorException("Security error occurred while accessing file '" + encryptionKeyFile
                + "'.", e);
        } catch (ClassNotFoundException e) {
            throw new ObjectEncryptorException("Failed to find one of classes in SecretKey's object graph, "
                + "while reading key from key file '" + encryptionKeyFile + "'.", e);
        } finally {
            Helper.closeStream(input);
            Helper.closeStream(output);
        }
    }

    /**
     * Gets encryption or decryption cipher.
     *
     * @param cipherMode the cipher mode
     * @param context the android context
     * @return the corresponding cipher
     * @throws IOException if I/O error occurs
     * @throws GeneralSecurityException if error related to cryptographic occurs
     * @throws ObjectEncryptorException if any other error occurs
     */
    private Cipher getCipher(int cipherMode, Context context) throws IOException, GeneralSecurityException,
        ObjectEncryptorException {
        // get encryption key from encryptionKeyFile file, or generate it for encryption
        SecretKey key = getKey(cipherMode == Cipher.ENCRYPT_MODE, context);

        // prepare the cipher
        Cipher cipher = Cipher.getInstance(encryptionAlgorithmName);
        cipher.init(cipherMode, key);
        return cipher;
    }
}